#pragma once


#include "ogl/gut/Shader.h"
#include "ogl/gut/Mesh.h"
#include "stb/stb_image.h"

#include "ogl/gut/types.h"

#include "ogl/pbrx/HdrTexture.h"


namespace Ogl
{
    namespace Pbrx
    {


        struct HdrTextureFactory {

            std::shared_ptr<Ogl::Gut::Shader> equirectangularToCubemapShader;
            std::shared_ptr<Ogl::Gut::Shader> irradianceShader;
            std::shared_ptr<Ogl::Gut::Shader> prefilterShader;
            std::shared_ptr<Ogl::Gut::Shader> brdfShader;

            std::shared_ptr<Ogl::Gut::Mesh> m_CubeMesh, m_QuadMesh;
            std::shared_ptr<Ogl::Gut::FrameBuffer> m_FrameBuffer;
            std::shared_ptr<Ogl::Gut::RenderBuffer> m_RenderBuffer;


            HdrTextureFactory()
            {
                Init();
            }
            void Init()
            {
                equirectangularToCubemapShader = std::make_shared<Ogl::Gut::Shader>(CubeMapVertCode().c_str(), EquirectangluarToCubeMapFragCode().c_str());
                irradianceShader = std::make_shared<Ogl::Gut::Shader>(CubeMapVertCode().c_str(), IrradianceConvolutionFragCode().c_str());
                prefilterShader = std::make_shared<Ogl::Gut::Shader>(CubeMapVertCode().c_str(), PrefilterFragCode().c_str());
                brdfShader = std::make_shared<Ogl::Gut::Shader>(BRDFVertCode().c_str(), BRDFFragCode().c_str());

                m_CubeMesh = std::make_shared<Ogl::Gut::Mesh>(Geometry::CreateBox());
                m_QuadMesh = std::make_shared<Ogl::Gut::Mesh>(Geometry::CreateQuad());

                m_FrameBuffer = std::make_shared<Ogl::Gut::FrameBuffer>();
                m_RenderBuffer = std::make_shared<Ogl::Gut::RenderBuffer>();

            }
            bool CreateHdrTexturesFromCubeMap(const Ogl::Gut::TextureCube* env, std::shared_ptr<Ogl::Pbrx::HdrTexture> target)
            {

                unsigned int& prefilterMap = target->prefilterMap->m_Id;
                unsigned int& irradianceMap = target->irradianceMap->m_Id;
                unsigned int& brdfLUTTexture = target->brdfLUTtexture->m_Id;

                m_FrameBuffer->Bind();
                m_RenderBuffer->Bind();
                m_RenderBuffer->Storage(GL_DEPTH_COMPONENT, 32, 32);


                // pbr: solve diffuse integral by convolution to create an irradiance (cube)map.
                // -----------------------------------------------------------------------------
                irradianceShader->Use();
                irradianceShader->setInt("environmentMap", 0);
                irradianceShader->setMat4("projection", captureProjection);
                target->envCubeMap->Active(0);

                glViewport(0, 0, 32, 32); // don't forget to configure the viewport to the capture dimensions.
                m_FrameBuffer->Bind();

                for (unsigned int i = 0; i < 6; ++i)
                {
                    irradianceShader->setMat4("view", captureViews[i]);
                    m_FrameBuffer->BufferTexture2D(GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);

                    //glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);
                    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

                    m_CubeMesh->Bind();
                    m_CubeMesh->DrawElementsAuto();;
                }
                Ogl::Gut::FrameBuffer::UnBind();;

                prefilterShader->Use();
                prefilterShader->setInt("environmentMap", 0);
                prefilterShader->setMat4("projection", captureProjection);
                target->envCubeMap->Active(0);

                m_FrameBuffer->Bind();

                unsigned int maxMipLevels = 5;
                for (unsigned int mip = 0; mip < maxMipLevels; ++mip)
                {
                    // reisze framebuffer according to mip-level size.
                    unsigned int mipWidth = static_cast<unsigned int>(128 * std::pow(0.5, mip));
                    unsigned int mipHeight = static_cast<unsigned int>(128 * std::pow(0.5, mip));
                    //glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
                    m_RenderBuffer->Bind();
                    //glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
                    m_RenderBuffer->Storage(GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
                    glViewport(0, 0, mipWidth, mipHeight);

                    float roughness = (float)mip / (float)(maxMipLevels - 1);
                    prefilterShader->setFloat("roughness", roughness);
                    for (unsigned int i = 0; i < 6; ++i)
                    {
                        prefilterShader->setMat4("view", captureViews[i]);
                        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip);

                        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                        m_CubeMesh->Bind();
                        m_CubeMesh->DrawElementsAuto();;
                    }
                }
                Ogl::Gut::FrameBuffer::UnBind();;


                m_FrameBuffer->Bind();
                m_RenderBuffer->Bind();

                m_RenderBuffer->Storage(GL_DEPTH_COMPONENT24, 512, 512);
                m_FrameBuffer->BufferTexture2D(GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0);


                glViewport(0, 0, 512, 512);
                brdfShader->Use();
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                m_QuadMesh->Bind();
                m_QuadMesh->DrawElementsAuto();
                return true;
            }

            bool CreateHdrTexture(std::shared_ptr<Ogl::Gut::TextureCube> cubeSource, std::shared_ptr<Ogl::Pbrx::HdrTexture>& target)
            {

                glEnable(GL_DEPTH_TEST);
                // set depth function to less than AND equal for skybox depth trick.
                glDepthFunc(GL_LEQUAL);
                // enable seamless cubemap sampling for lower mip levels in the pre-filter map.
                // glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
                glEnable(0x884F);

                target->envCubeMap = cubeSource;
                CreateHdrTexturesFromCubeMap(target->envCubeMap.get(), target);
                Ogl::Gut::FrameBuffer::UnBind();;


                return true;
            };

            bool CreateHdrTexture(const Ogl::Gut::Texture2D* hdrSource,std::shared_ptr<Ogl::Pbrx::HdrTexture> target)
            {


                unsigned int& prefilterMap = target->prefilterMap->m_Id;
                unsigned int& irradianceMap = target->irradianceMap->m_Id;
                unsigned int& brdfLUTTexture = target->brdfLUTtexture->m_Id;


                m_FrameBuffer->Bind();
                m_RenderBuffer->Bind();

                m_RenderBuffer->Storage(GL_DEPTH_COMPONENT24, 512, 512);
                m_FrameBuffer->FrameRenderBuffer(GL_DEPTH_ATTACHMENT, m_RenderBuffer->ID);


                // pbr: convert HDR equirectangular environment map to cubemap equivalent
                // ----------------------------------------------------------------------
                equirectangularToCubemapShader->Use();
                equirectangularToCubemapShader->setInt("equirectangularMap", 0);
                equirectangularToCubemapShader->setMat4("projection", captureProjection);

                hdrSource->Active(0);

                glViewport(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions.
                m_FrameBuffer->Bind();

                for (unsigned int i = 0; i < 6; ++i)
                {
                    equirectangularToCubemapShader->setMat4("view", captureViews[i]);
                    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, target->envCubeMap->m_Id, 0);
                    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

                    m_CubeMesh->Bind();
                    m_CubeMesh->DrawElementsAuto();;
                }
                Ogl::Gut::FrameBuffer::UnBind();;

                target->envCubeMap->Bind();
                target->envCubeMap->Mipmap();

                CreateHdrTexturesFromCubeMap(target->envCubeMap.get(), target);

                Ogl::Gut::FrameBuffer::UnBind();;

                return true;

            }

            static std::string CubeMapVertCode();
            static std::string EquirectangluarToCubeMapFragCode();
            static std::string IrradianceConvolutionFragCode();
            static std::string BRDFFragCode();
            static std::string BRDFVertCode();
            static std::string PrefilterFragCode();

        private:

            glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
            glm::mat4 captureViews[6] =
            {
                glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
                glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
                glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
                glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
                glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
                glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))
            };
        };
    }
}